package fm.last
{
    import com.adobe.crypto.MD5;
    
    import flash.events.Event;
    import flash.events.IOErrorEvent;
    import flash.events.SecurityErrorEvent;
    import flash.net.URLLoader;
    import flash.net.URLRequest;
    import flash.net.URLVariables;
    import flash.utils.unescapeMultiByte;
    
    import fm.last.api.AuthAPI;
    import fm.last.error.APIError;
    import fm.last.error.AuthError;
    
    
    /** 
     *  
     *  Base API for all Last.fm <a href="http://www.last.fm/api/rest">REST-APIs</a>.
     *   
     *  This class defines the common http/rpc request.
     *  The inheritance classes (in <code>fm.last.api</code>) define corresponding arguments/parameters.
     * 
     *  see the document of <code>LastFmService</code> to use last.fm's APIs.   
     */
    public class RestAPI
    {
        /** API URL */                                          
        private static const API_URL:String = "http://ws.audioscrobbler.com/2.0/";
        
        /** AUTH URL */
        private static const AUTH_URL:String = "http://www.last.fm/api/auth/";        
                        
        /** agent object */
        private var _agent:Agent = null;

        /* ********************************************************************/

        /** Constructor
         *  @param agent <code>Agent</code> object.    
         */
        public function RestAPI(agent:Agent) 
        {
            this._agent = agent;
        }                
        
        /** 
        * Convert URLVariables object to a plain string all key,valaue are appended together.
        * The key values are sorted before the conversion of the string. 
        */
        public static function urlVarToSortedString(variables:URLVariables):String
        {
            var urlVarString:String = variables.toString();
            //trace("[DEBUG] URLVariables: " + urlVarString);
                        
            var varPairs:Array = urlVarString.split('&');            
            varPairs.sort();
            //trace("[DEBUG] sort splitted pairs: " + varPairs);
            
            var result:String = varPairs.toString()
            result = result.replace(/[=,]/g,"");
            //trace("[DEBUG] url variable (escaped) string: " + result);
            
            return result;
        }
        
        /**
         * create API signature. 
         */
        private static function createApiSignature(secrete:String, variables:URLVariables):String
        {
            if (secrete == null) {
                throw new AuthError("SECRET is necessary to call " + variables["method"] + " API");
            } 
            var str:String = urlVarToSortedString(variables) + secrete;
            
            str = unescapeMultiByte(str);
            //trace("[DEBUG] final (unescaped) string:" + str);
            
            str = com.adobe.crypto.MD5.hash(str);
            //trace("[DEBUG] final sig:" + str);
            
            return str;        
        }
        
    
        
        /**
         * Request API call.
         * 
         * @param urlReqMethod <code>URLRequestMethod.GET</code> or <code>URLRequestMethod.GET</code>
         * @param urlVars      variables for requesting <code>API_URL</code>.
         * @param isAuth       if auth is required
         * @param onComplete   callback function when the request completes.  
         * @param onError      callback function when any error occurs.
         */
        protected function requestAPI(urlReqMethod:String,                                       
                                      urlVars:URLVariables,
                                      isAuth:Boolean,
                                      onComplete:Function=null,
                                      onError:Function=null):void
        {        
            /* ***************** urlvariables ********************/                                
                                                
            /* append common variables for APIs */            
            urlVars["api_key"] = _agent._api_key;
                        
            if (urlVars["method"] == AuthAPI.AUTH_GET_SESSION) { 
                /* append signature if auth.getSession is called */   
                             
                urlVars["api_sig"] = createApiSignature(_agent._secret, urlVars);
                
            } else if (isAuth) {
                /* append signature if auth is required */
                                
                if (_agent._sk == null) {
                    throw new AuthError("there is no SESSION KEY available for " + urlVars["method"] + " API");    
                }
                urlVars["sk"]      = _agent._sk;                
                // assign signature AT THE LAST STEP !!                
                urlVars["api_sig"] = createApiSignature(_agent._secret, urlVars);
                
            } 
            // NO MORE variable is allowed to add for the request. (the variables are signed.) 
            
            /* ***************** urlrequest ********************/
            
            /* request object */                    
            var request:URLRequest = new URLRequest(API_URL);
            request.data = urlVars;                
            request.method = urlReqMethod;            
                        
            //trace("request headers:" + request.requestHeaders);
            //trace("request url/data: " + request.url + "?" + request.data);
            //trace("request method:" + request.method);
            //trace("request content-type:" + request.contentType);
            //trace("request digest:" + request.digest);            
            
            /* ***************** urlloader ********************/
            
            var urlLoader:URLLoader = new URLLoader();
            
            var _completed:Function = function(e:Event):void {
                urlLoader.removeEventListener(Event.COMPLETE, _completed);
                
                /* expected response: <lfm status="ok|failed">...</lfm> */
                var xml:XML = new XML(e.target.data);                
                if (xml.attribute("status") != "ok" && xml.attribute("status") != "failed") {
                    throw new APIError("Web API response has unexpeted status:" + xml.toString());
                } 
                if (onComplete != null) onComplete(xml);    
            };
            
            var _ioError:Function = function(e:IOErrorEvent):void {
                urlLoader.removeEventListener(IOErrorEvent.IO_ERROR, _ioError);
                
                e.text =  "[" + request.url + "?" + request.data + "]\n" + e.text;
                if (onError != null) onError(e);
            };
            
            var _secureError:Function = function(e:SecurityErrorEvent):void {
                urlLoader.removeEventListener(SecurityErrorEvent.SECURITY_ERROR, _secureError);
                
                e.text =  "[" + request.url + "?" + request.data + "]\n" + e.text;
                if (onError != null) onError(e);
            };
            
            /* handle events */            
            urlLoader.addEventListener(Event.COMPLETE, _completed);                                    
            urlLoader.addEventListener(IOErrorEvent.IO_ERROR, _ioError);            
            urlLoader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, _secureError);
            
//            urlLoader.addEventListener(HTTPStatusEvent.HTTP_STATUS, ...);
//            urlLoader.addEventListener(Event.OPEN, ...);
//            urlLoader.addEventListener(ProgressEvent.PROGRESS, ...);
            
            /* ********** request ***********************/
                                                
            /* do request */
            urlLoader.load(request);            
        }        
        
    }
}